package job

import (
	"fmt"
	"gcrontab/common"
	"time"
)

type Scheduler struct {
	jobEventChan      chan *common.JobEvent              //ETCD任务事件队列
	jobPlanTable      map[string]*common.JobSchedulePlan //任务调度计划map
	jobExecutingTable map[string]*common.JobExecuteInfo  //任务执行状态map
	jobResultChan     chan *common.JobExecuteResult      //任务执行结果
}

var G_scheduler *Scheduler

//初始化任务调度器
func InitScheduler() error {
	G_scheduler = &Scheduler{
		jobEventChan:      make(chan *common.JobEvent, 1000),
		jobPlanTable:      make(map[string]*common.JobSchedulePlan),
		jobExecutingTable: make(map[string]*common.JobExecuteInfo),
		jobResultChan:     make(chan *common.JobExecuteResult, 1000),
	}
	go G_scheduler.schedulerLoop()

	return nil
}

//推送 任务变化事件
func (s *Scheduler) PushJobEvent(jobEvent *common.JobEvent) {
	fmt.Println("任务变化事件", jobEvent.Job.Name)
	s.jobEventChan <- jobEvent
}

//回传任务执行结果
func (s *Scheduler) PushJobResult(jobResult *common.JobExecuteResult) {
	s.jobResultChan <- jobResult
}

//任务调度协程
func (s *Scheduler) schedulerLoop() {
	//初始化
	ScheduleAfter := s.TrySchedule()
	//定时器
	ScheduleTimer := time.NewTimer(ScheduleAfter)
	for {
		select {
		case jobEvent := <-s.jobEventChan: //监听 任务变化事件
			s.handleJobEvent(jobEvent) //（MARK3）
		case jobResult := <-s.jobResultChan: //监听 任务执行结果
			s.handleJobResult(jobResult)     //（MARK6）
		case <-ScheduleTimer.C: //每隔ScheduleAfter秒执行一次
		}
		//调度一次任务
		ScheduleAfter = s.TrySchedule()
		//重置定时器
		ScheduleTimer.Reset(ScheduleAfter)
	}
}

//计算过期时间和执行到期任务
func (s *Scheduler) TrySchedule() (scheduleAfter time.Duration) {
	var now time.Time
	var nearTime *time.Time
	//任务为空
	if len(s.jobPlanTable) <= 0 {
		scheduleAfter = 1 * time.Second
		return
	}

	now = time.Now()
	//遍历所有任务
	for _, jobPlan := range s.jobPlanTable {
		if jobPlan.NextTime.Before(now) || jobPlan.NextTime.Equal(now) {
			//过期的任务立即执行 （MARK4）
			s.TryStartJob(jobPlan)
			jobPlan.NextTime = jobPlan.Expr.Next(now)
		}
		//统计最近的要过期的任务的时间
		if nearTime == nil || jobPlan.NextTime.Before(*nearTime) {
			nearTime = &jobPlan.NextTime
		}
	}

	//下次调度时间间隔(最近的要执行的任务时间 - 当前时间)
	scheduleAfter = (*nearTime).Sub(now)
	return
}

//处理 任务变化事件
func (s *Scheduler) handleJobEvent(jobEvent *common.JobEvent) {
	switch jobEvent.EventType {
	case common.JOB_EVENT_SAVE:
		jobSchedulePlan, err := common.BuildJobSchedulePlan(jobEvent.Job)
		if err != nil {
			return
		}
		//存入map
		s.jobPlanTable[jobEvent.Job.Name] = jobSchedulePlan
	case common.JOB_EVENT_DELETE:
		//如果任务存在，则删除
		if _, ok := s.jobPlanTable[jobEvent.Job.Name]; ok {
			delete(s.jobPlanTable, jobEvent.Job.Name)
		}
	case common.JOB_EVENT_KILLER:
		//如果任务存在，则删除
		if jobExecuteInfo, ok := s.jobExecutingTable[jobEvent.Job.Name]; ok {
			//取消command执行
			jobExecuteInfo.CancelFunc()
		}
	}
}

//尝试执行任务
//ps: 调度和执行是两件事
//ps: 执行的任务可能很久，一分钟调度60次, 但是只能执行一次, 防止并发
func (s *Scheduler) TryStartJob(jobPlan *common.JobSchedulePlan) {
	//检查某一任务是否正在执行，有则跳过本次任务
	if _, ok := s.jobExecutingTable[jobPlan.Job.Name]; ok {
		fmt.Println("1，正在执行：", jobPlan.Job.Name)
		return
	}
	//构建执行状态
	jobExecutingInfo := common.BuildJobExecuteInfo(jobPlan)
	//保存执行信息
	s.jobExecutingTable[jobPlan.Job.Name] = jobExecutingInfo
	//执行任务 （MARK5）
	G_executor.ExecuteJob(jobExecutingInfo)
	fmt.Println("2，执行任务：", jobExecutingInfo.Job.Name)
}

//删除已经执行完成的任务
func (s *Scheduler) handleJobResult(result *common.JobExecuteResult) {
	delete(s.jobExecutingTable, result.ExecuteInfo.Job.Name)
	if result.Err != common.ERR_JOB_LOCK {
		jobLog := &common.JobLog{
			JobName:      result.ExecuteInfo.Job.Name,
			Command:      result.ExecuteInfo.Job.Command,
			Output:       string(result.Output),
			PlanTime:     result.ExecuteInfo.PlanTime.UnixNano() / (1000 * 1000),
			ScheduleTime: result.ExecuteInfo.RealTime.UnixNano() / (1000 * 1000),
			StartTime:    result.StartTime.UnixNano() / (1000 * 1000),
			EndTime:      result.EndTime.UnixNano() / (1000 * 1000),
		}
		if result.Err != nil {
			jobLog.Err = result.Err.Error()
		} else {
			jobLog.Err = ""
		}
		//todo 将日志存入到mongoDB中
		G_logSink.AppendLog(jobLog)
	}
	fmt.Println("任务执行完成：", result.ExecuteInfo.Job.Name, string(result.Output), result.Err)
}
